/*
 * Copyright (c) 2009, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.wso2.carbon.bpel.b4p.utils;

import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.impl.builder.StAXOMBuilder;
import org.apache.axis2.AxisFault;
import org.apache.axis2.client.Options;
import org.apache.axis2.deployment.ServiceBuilder;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.description.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ode.bpel.iapi.*;
import org.apache.ode.bpel.epr.EndpointFactory;
import org.apache.ode.bpel.epr.MutableEndpoint;
import org.apache.ode.utils.DOMUtils;
import org.apache.ode.utils.Namespaces;
import org.apache.ode.utils.stl.CollectionsX;
import org.apache.neethi.Policy;
import org.apache.neethi.PolicyEngine;
import org.apache.ws.commons.schema.XmlSchema;
import org.apache.ws.commons.schema.XmlSchemaCollection;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import javax.wsdl.Definition;
import javax.wsdl.Port;
import javax.wsdl.Service;
import javax.wsdl.Binding;
import javax.wsdl.extensions.UnknownExtensibilityElement;
import javax.wsdl.extensions.ExtensibilityElement;
import javax.wsdl.extensions.soap12.SOAP12Binding;
import javax.wsdl.extensions.http.HTTPAddress;
import javax.wsdl.extensions.http.HTTPBinding;
import javax.wsdl.extensions.soap.SOAPAddress;
import javax.wsdl.extensions.soap.SOAPBinding;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;
import java.io.InputStream;
import java.io.IOException;
import java.util.Iterator;
import java.util.Collection;
import java.util.ArrayList;

public class HumanServiceUtil {
    private static final Log log = LogFactory.getLog(HumanServiceUtil.class);

    public static final String TARGET_SESSION_ENDPOINT = "targetSessionEndpoint";
    public static final String CALLBACK_SESSION_ENDPOINT = "callbackSessionEndpoint";

    public static final String CONFIGURED_USING_BPEL_PKG_CONFIG_FILES ="confgiuredUsingBpelPkgFiles";

    public static final String MODULE_RAMPART = "rampart";
    public static final String MODULE_RAHAS = "rahas";

    /**
     * Get the EPR of this service from the WSDL.
     *
     * @param wsdlDef     WSDL Definition
     * @param serviceName service name
     * @param portName    port name
     * @return XML representation of the EPR
     */
    public static Element genEPRfromWSDL(Definition wsdlDef, QName serviceName, String portName) {
        Service serviceDef = wsdlDef.getService(serviceName);
        if (serviceDef != null) {
            Port portDef = serviceDef.getPort(portName);
            if (portDef != null) {
                Document doc = DOMUtils.newDocument();
                Element service = doc.createElementNS(Namespaces.WSDL_11, "service");
                service.setAttribute("name", serviceDef.getQName().getLocalPart());
                service.setAttribute("targetNamespace", serviceDef.getQName().getNamespaceURI());
                Element port = doc.createElementNS(Namespaces.WSDL_11, "port");
                service.appendChild(port);
                port.setAttribute("name", portDef.getName());
                port.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:bindns", portDef.getBinding().getQName()
                        .getNamespaceURI());
                port.setAttribute("bindns:binding", portDef.getName());
                for (Object extElmt : portDef.getExtensibilityElements()) {
                    if (extElmt instanceof SOAPAddress) {
                        Element soapAddr = doc.createElementNS(Namespaces.SOAP_NS, "address");
                        port.appendChild(soapAddr);
                        soapAddr.setAttribute("location", ((SOAPAddress) extElmt).getLocationURI());
                    } else if (extElmt instanceof HTTPAddress) {
                        Element httpAddr = doc.createElementNS(Namespaces.HTTP_NS, "address");
                        port.appendChild(httpAddr);
                        httpAddr.setAttribute("location", ((HTTPAddress) extElmt).getLocationURI());
                    } else {
                        port.appendChild(doc.importNode(((UnknownExtensibilityElement) extElmt).getElement(), true));
                    }
                }
                return service;
            }
        }
        return null;
    }

    /**
     * Create-and-copy a service-ref element.
     *
     * @param elmt Service Reference element
     * @return wrapped element
     */
    public static MutableEndpoint createServiceRef(Element elmt) {
        Document doc = DOMUtils.newDocument();
        QName elQName = new QName(elmt.getNamespaceURI(), elmt.getLocalName());
        // If we get a service-ref, just copy it, otherwise make a service-ref
        // wrapper
        if (!EndpointReference.SERVICE_REF_QNAME.equals(elQName)) {
            Element serviceref = doc.createElementNS(EndpointReference.SERVICE_REF_QNAME.getNamespaceURI(),
                    EndpointReference.SERVICE_REF_QNAME.getLocalPart());
            serviceref.appendChild(doc.importNode(elmt, true));
            doc.appendChild(serviceref);
        } else {
            doc.appendChild(doc.importNode(elmt, true));
        }

        return EndpointFactory.createEndpoint(doc.getDocumentElement());
    }

    /**
     * Configure a service instance woth the specified service.xml document.
     * If modules are mentioned in the document, <code>this</code> method will make sure they are properly engaged and engage them if necessary.
     * The modules have to be available in the module repository otherwise an AxisFault will be thrown.
     *
     * @param configCtx    Server configuration context
     * @param axisService  the service to configure
     * @param service_file the service.xm document to configure the service with
     * @param basePath     base direcotry of the bpel package
     * @throws java.io.IOException        if error ocurred during opening stream
     * @throws javax.xml.stream.XMLStreamException
     *                                    when error reading XML stream
     * @throws org.apache.axis2.AxisFault if a module listed in the service.xml is not available in the module repository
     */
    public static void configureService(ConfigurationContext configCtx, AxisService axisService, String service_file, String basePath) throws IOException, XMLStreamException {
        FileLoadingUtil fileLoader = new FileLoadingUtil(basePath);
        InputStream serviceDescIs = null;
        try {
            serviceDescIs = fileLoader.load(service_file);
        } catch (FileLoadingUtilException e) {
            log.error("Service Description loading error.", e);
        }

        if (log.isDebugEnabled()) {
            log.debug("Looking for Axis2 service configuration file: " + service_file);
        }
        if (serviceDescIs != null) {
            if (log.isDebugEnabled()) {
                log.debug("Configuring service " + axisService.getName() + " using: " + service_file);
            }
            try {
                if (configCtx == null)
                    configCtx = new ConfigurationContext(axisService.getAxisConfiguration());
                ServiceBuilder builder = new ServiceBuilder(configCtx, axisService);
                StAXOMBuilder omBuilder = new StAXOMBuilder(serviceDescIs);
                OMElement documentEle = omBuilder.getDocumentElement();
                Iterator itr = documentEle.getChildElements();

                while (itr.hasNext()) {
                    OMElement serviceEle = (OMElement) itr.next();
                    if (serviceEle.getLocalName().toLowerCase().equals("service")) {
                        if (serviceEle.getAttribute(new QName("name")) != null &&
                                serviceEle.getAttribute(new QName("name")).getAttributeValue().equals(axisService.getName())) {

                            builder.populateService(serviceEle);
                            // This is a hack to avoid security configurations get persisted when we configure using
                            // services.xml file or policy.xml file BPEL package. But this should be fix at the
                            // Carbon Persistence manager.
                            Parameter param = new Parameter(CONFIGURED_USING_BPEL_PKG_CONFIG_FILES, "true");
                            axisService.addParameter(param);

                        } else if (serviceEle.getAttribute(new QName("name")) != null && // Starts with check is used to handle anonymous services generated for external service.
                                axisService.getName().startsWith("axis_service_for_" + serviceEle.getAttributeValue(new QName("name")))) {
                            String svcName = axisService.getName();
                            AxisService axisSvc = builder.populateService(serviceEle);
                            axisSvc.setName(svcName);

                        }
                    }
                }

            } finally {
                serviceDescIs.close();
            }
            // the service builder only updates the module list but do not engage them
            // modules have to be engaged manually,
            for (int i = 0; i < axisService.getModules().size(); i++) {
                String moduleRef = axisService.getModules().get(i);
                AxisModule module = axisService.getAxisConfiguration().getModule(moduleRef);
                if (module != null) {
                    axisService.engageModule(module);
                } else {
                    throw new AxisFault("Unable to engage module: " + moduleRef);
                }
            }
        }
    }

    public static void applySecurityPolicy(AxisService service, String policy_file, String basePath) {
        FileLoadingUtil fileLoader = new FileLoadingUtil(basePath);
        if (log.isDebugEnabled()) log.debug("Applying security policy: " + policy_file);
        try {
            InputStream policyStream = fileLoader.load(policy_file);
            try {
                Policy policyDoc = PolicyEngine.getPolicy(policyStream);
                service.getPolicySubject().attachPolicy(policyDoc);
                // make sure the proper modules are engaged, if they are available
                engageModules(service, "rampart", "rahas");
            } finally {
                policyStream.close();
            }

            // This is a hack to avoid security configurations get persisted when we configure using
            // services.xml file or policy.xml file BPEL package. But this should be fix at the
            // Carbon Persistence manager.
            Parameter param = new Parameter(CONFIGURED_USING_BPEL_PKG_CONFIG_FILES, "true");
            service.addParameter(param);
        } catch (IOException e) {
            log.error("Fialed to attach policy to service. Exception while parsing policy: " + policy_file, e);
        } catch (FileLoadingUtilException e) {
            log.error("Fialed to attach policy to service. Error locating policy file " + policy_file, e);
        }
    }

    public static void engageModules(AxisDescription description, String... modules) throws AxisFault {
        for (String m : modules) {
            if (description.getAxisConfiguration().getModule(m) != null) {
                if (!description.getAxisConfiguration().isEngaged(m) && !description.isEngaged(m)) {
                    description.engageModule(description.getAxisConfiguration().getModule(m));
                }
            } else {
                if (log.isDebugEnabled()) log.debug("Module " + m + " is not available.");
            }
        }
    }

    /**
     * Axis2 monkey patching to force the usage of the read(element,baseUri) method
     * of XmlSchema as the normal read is broken.
     */
    public static class WSDL11ToAxisPatchedBuilder extends WSDL11ToAxisServiceBuilder {
        public WSDL11ToAxisPatchedBuilder(InputStream in, QName serviceName, String portName) {
            super(in, serviceName, portName);
        }

        public WSDL11ToAxisPatchedBuilder(Definition def, QName serviceName, String portName) {
            super(def, serviceName, portName);
        }

        public WSDL11ToAxisPatchedBuilder(Definition def, QName serviceName, String portName, boolean isAllPorts) {
            super(def, serviceName, portName, isAllPorts);
        }

        public WSDL11ToAxisPatchedBuilder(InputStream in, AxisService service) {
            super(in, service);
        }

        public WSDL11ToAxisPatchedBuilder(InputStream in) {
            super(in);
        }

        protected XmlSchema getXMLSchema(Element element, String baseUri) {
            XmlSchemaCollection schemaCollection = new XmlSchemaCollection();
            if (baseUri != null) {
                schemaCollection.setBaseUri(baseUri);
            }
            return schemaCollection.read(element, baseUri);
        }
    }

    public static ExtensibilityElement getBindingExtension(Binding binding) {
        Collection bindings = new ArrayList();
        CollectionsX.filter(bindings, binding.getExtensibilityElements(), HTTPBinding.class);
        CollectionsX.filter(bindings, binding.getExtensibilityElements(), SOAPBinding.class);
        CollectionsX.filter(bindings, binding.getExtensibilityElements(), SOAP12Binding.class);
        if (bindings.size() == 0) {
            return null;
        } else if (bindings.size() > 1) {
            // exception if multiple bindings found
            throw new IllegalArgumentException("Multiple bindings: " + binding.getQName());
        } else {
            // retrieve the single element
            ExtensibilityElement result = (ExtensibilityElement) bindings.iterator().next();
            return result;
        }
    }

     /**
     * Clones the given {@link org.apache.axis2.client.Options} object. This is not a deep copy
     * because this will be called for each and every message going out from synapse. The parent
     * of the cloning options object is kept as a reference.
     *
     * @param options clonning object
     * @return clonned Options object
     */
    public static Options cloneOptions(Options options) {

        // create new options object and set the parent
        Options clonedOptions = new Options(options.getParent());

        // copy general options
        clonedOptions.setCallTransportCleanup(options.isCallTransportCleanup());
        clonedOptions.setExceptionToBeThrownOnSOAPFault(options.isExceptionToBeThrownOnSOAPFault());
        clonedOptions.setManageSession(options.isManageSession());
        clonedOptions.setSoapVersionURI(options.getSoapVersionURI());
        clonedOptions.setTimeOutInMilliSeconds(options.getTimeOutInMilliSeconds());
        clonedOptions.setUseSeparateListener(options.isUseSeparateListener());

        // copy transport related options
        clonedOptions.setListener(options.getListener());
        clonedOptions.setTransportIn(options.getTransportIn());
        clonedOptions.setTransportInProtocol(options.getTransportInProtocol());
        clonedOptions.setTransportOut(clonedOptions.getTransportOut());

        // copy username and password options
        clonedOptions.setUserName(options.getUserName());
        clonedOptions.setPassword(options.getPassword());

        // cloen the property set of the current options object
        for (Object o : options.getProperties().keySet()) {
            String key = (String) o;
            clonedOptions.setProperty(key, options.getProperty(key));
        }

        return clonedOptions;
    }
}